package io.lvdaxian.mybatis11.binding;

import io.lvdaxian.mybatis11.mapping.MappedStatement;
import io.lvdaxian.mybatis11.mapping.SqlCommandType;
import io.lvdaxian.mybatis11.session.Configuration;
import io.lvdaxian.mybatis11.session.SqlSession;

import java.lang.reflect.Method;
import java.util.*;

/**
 * 这是一个执行方法映射
 *
 * @author lihh
 */
public class MapperMethod {
  private final SqlCommand command;
  private final MethodSignature method;
  
  public MapperMethod(Class<?> mapperInterface, Method method, Configuration configuration) {
    this.command = new SqlCommand(configuration, mapperInterface, method);
    this.method = new MethodSignature(configuration, method);
  }
  
  /**
   * 执行参数的时候
   *
   * @param sqlSession sql session
   * @param args       调用方法的时候 传递的参数
   * @return 这里返回对象
   * @author lihh
   */
  public Object execute(SqlSession sqlSession, Object[] args) {
    Object result = null;
    switch (command.getType()) {
      case INSERT: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = sqlSession.insert(command.getName(), param);
        break;
      }
      case DELETE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = sqlSession.delete(command.getName(), param);
        break;
      }
      case UPDATE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = sqlSession.update(command.getName(), param);
        break;
      }
      case SELECT: {
        Object param = method.convertArgsToSqlCommandParam(args);
        if (method.returnsMany) {
          result = sqlSession.selectList(command.getName(), param);
        } else {
          result = sqlSession.selectOne(command.getName(), param);
        }
        break;
      }
      default:
        throw new RuntimeException("Unknown execution method for: " + command.getName());
    }
    return result;
  }
  
  /**
   * 表示 sql 命令
   *
   * @author lihh
   */
  public static class SqlCommand {
    private final String name;
    private final SqlCommandType type;
    
    public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
      // 类的限定名 + 方法的名称
      String statementName = mapperInterface.getName() + "." + method.getName();
      // 拿到 mapped statement 片段
      MappedStatement ms = configuration.getMappedStatement(statementName);
      
      this.name = ms.getId();
      type = ms.getSqlCommandType();
    }
    
    public String getName() {
      return name;
    }
    
    public SqlCommandType getType() {
      return type;
    }
  }
  
  /**
   * 方法签名
   *
   * @author lihh
   */
  public static class MethodSignature {
    private final boolean returnsMany;
    private final Class<?> returnType;
    private final SortedMap<Integer, String> params;
    
    public MethodSignature(Configuration configuration, Method method) {
      this.returnType = method.getReturnType();
      this.returnsMany = (configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray());
      this.params = Collections.unmodifiableSortedMap(getParams(method));
    }
    
    /**
     * 转换 sql command param
     *
     * @param args 参数args
     * @return 返回一个对象
     * @author lihh
     */
    public Object convertArgsToSqlCommandParam(Object[] args) {
      final int paramCount = args.length;
      // 表示没有参数
      if (0 == paramCount)
        return null;
      
      // 这是只有一个参数的情况
      if (1 == paramCount)
        return args[params.keySet().iterator().next()];
      
      // 否则，返回一个ParamMap，修改参数名，参数名就是其位置
      final Map<String, Object> param = new ParamMap<>();
      int i = 0;
      for (Map.Entry<Integer, String> entry : params.entrySet()) {
        // 1.先加一个#{0},#{1},#{2}...参数
        param.put(entry.getValue(), args[entry.getKey().intValue()]);
        
        // issue #71, add param names as param1, param2...but ensure backward compatibility
        final String genericParamName = "param" + (i + 1);
        if (!param.containsKey(genericParamName))
          /*
           * 2.再加一个#{param1},#{param2}...参数
           * 你可以传递多个参数给一个映射器方法。如果你这样做了,
           * 默认情况下它们将会以它们在参数列表中的位置来命名,比如:#{param1},#{param2}等。
           * 如果你想改变参数的名称(只在多参数情况下) ,那么你可以在参数上使用@Param(“paramName”)注解。
           */
          param.put(genericParamName, args[entry.getKey()]);
      }
      
      return params;
    }
    
    
    private SortedMap<Integer, String> getParams(Method method) {
      // 用一个TreeMap，这样就保证还是按参数的先后顺序
      final SortedMap<Integer, String> params = new TreeMap<Integer, String>();
      // 这里拿到 method 参数类型
      final Class<?>[] argTypes = method.getParameterTypes();
      for (int i = 0; i < argTypes.length; i++) {
        String paramName = String.valueOf(params.size());
        // 不做 Param 的实现，这部分不处理。如果扩展学习，需要添加 Param 注解并做扩展实现。
        params.put(i, paramName);
      }
      return params;
    }
    
    public boolean returnsMany() {
      return returnsMany;
    }
  }
  
  /**
   * 参数map，静态内部类,更严格的get方法，如果没有相应的key，报错
   */
  public static class ParamMap<V> extends HashMap<String, V> {
    
    private static final long serialVersionUID = -2212268410512043556L;
    
    @Override
    public V get(Object key) {
      if (!super.containsKey(key)) {
        throw new RuntimeException("Parameter '" + key + "' not found. Available parameters are " + keySet());
      }
      return super.get(key);
    }
    
  }
}
